昨天學習了 Android 的版面配置、View Binding,以及 Activity 與 Fragment 的基本操作。今天要繼續認識 Android 常用的介面元件(Widget),並透過四個範例練習 TextView、Button、ImageView 與 ImageButton 的互動方式。
今天會完成:
when 判斷是哪一個按鈕被點擊。ImageView 動態切換圖片。ImageButton 建立圖片按鈕。參考資料:Android Developers-View Binding|Android Developers-Button
Android Widget 是放在 App 畫面上的基本介面元件,例如:
| Widget | 用途 |
|---|---|
TextView |
顯示文字或數字 |
Button |
一般文字按鈕 |
ImageView |
顯示圖片 |
ImageButton |
可以被點擊的圖片按鈕 |
EditText |
讓使用者輸入文字 |
CheckBox |
提供多選項目 |
RadioButton |
提供單選項目 |
Switch |
切換開啟或關閉狀態 |
我們可以在 XML 中決定元件的外觀與位置,再由 Kotlin 程式設定點擊事件。當使用者按下按鈕時,Android 便會呼叫對應的 OnClickListener。
參考資料:Android Developers-使用 Views 建構版面|Android Developers-輸入事件
第一個範例延續昨天的 View Binding。畫面中有三個 Button 與三個 TextView,按下不同按鈕後,修改對應的文字。
package com.example.layout3
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.layout3.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// 宣告 View Binding。
// lateinit 表示稍後會在 onCreate() 中完成初始化。
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge,讓 App 內容可以延伸到系統列區域。
enableEdgeToEdge()
// 根據 activity_main.xml 建立 Binding 物件。
binding = ActivityMainBinding.inflate(layoutInflater)
// 將 XML 的根 View 設定為 Activity 畫面。
setContentView(binding.root)
// 取得狀態列及導覽列的 Insets,
// 並將其設為根版面的 Padding,避免內容遭系統列遮住。
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 按下第一個按鈕,修改第一個 TextView。
binding.button1.setOnClickListener {
binding.textViewText1.text = "Button-1 Clicked"
}
// 按下第二個按鈕,修改第二個 TextView。
binding.button2.setOnClickListener {
binding.textViewText2.text = "Button-2 Clicked"
}
// 按下第三個按鈕,修改第三個 TextView。
binding.button3.setOnClickListener {
binding.textViewText3.text = "Button-3 Clicked"
}
}
}
View Binding 會根據 XML 檔名自動產生 Binding 類別。例如 activity_main.xml 會產生 ActivityMainBinding,XML 中的 button1 則可以透過 binding.button1 直接存取。
參考資料:Android Developers-在 Activity 使用 View Binding
範例使用以下方式處理按鈕點擊:
binding.button1.setOnClickListener {
binding.textViewText1.text = "Button-1 Clicked"
}
setOnClickListener 需要接收一個 View.OnClickListener。因為 OnClickListener 只有一個抽象函式,所以 Kotlin 可以利用 SAM 轉換,將完整的匿名物件簡化成 Lambda。
完整匿名物件寫法如下:
binding.button1.setOnClickListener(
object : android.view.View.OnClickListener {
override fun onClick(view: android.view.View?) {
binding.textViewText1.text = "Button-1 Clicked"
}
}
)
Lambda 寫法可以減少重複的樣板程式碼,使按鈕行為更容易閱讀。
參考資料:Android Developers-回應按鈕點擊事件
第二個範例要製作一個簡單的計數器,畫面中包含:
TextView。button_plus。button_minus。button_reset。這次不為每個按鈕分別建立 Lambda,而是讓三個按鈕共用同一個 MyButton 事件監聽器。
參考資料:Android Developers-View.OnClickListener
package com.example.widget_2
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
// 宣告三個功能按鈕。
private lateinit var buttonReset: Button
private lateinit var buttonMinus: Button
private lateinit var buttonPlus: Button
// 顯示目前計數結果的 TextView。
private lateinit var textViewCounter: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge 顯示。
enableEdgeToEdge()
// 載入 activity_main.xml。
setContentView(R.layout.activity_main)
// 處理系統狀態列與導覽列的範圍。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 使用 findViewById() 取得 XML 中的畫面元件。
textViewCounter = findViewById(R.id.txt_CountValue)
buttonPlus = findViewById(R.id.button_plus)
buttonMinus = findViewById(R.id.button_minus)
buttonReset = findViewById(R.id.button_reset)
// 三個按鈕共用 MyButton 事件監聽器。
buttonPlus.setOnClickListener(MyButton())
buttonMinus.setOnClickListener(MyButton())
buttonReset.setOnClickListener(MyButton())
}
/**
* 共用的按鈕事件監聽器。
*
* 使用 inner 宣告內部類別後,MyButton 才能直接存取
* MainActivity 中的 textViewCounter 等成員。
*/
inner class MyButton : View.OnClickListener {
override fun onClick(view: View?) {
// 透過被點擊 View 的 ID,判斷使用者按了哪個按鈕。
when (view?.id) {
R.id.button_plus -> {
// 先將 TextView 顯示的文字轉成 String。
val data = textViewCounter.text.toString()
// 再將 String 轉成 Int,才能執行加法。
var number = data.toInt()
// 數值加一。
number++
// TextView.text 需要文字,因此再將 Int 轉回 String。
textViewCounter.text = number.toString()
}
R.id.button_minus -> {
// 讀取畫面目前顯示的數值。
val data = textViewCounter.text.toString()
var number = data.toInt()
// 只有大於零時才減一,避免出現負數。
if (number > 0) {
number--
}
// 將計算結果顯示回 TextView。
textViewCounter.text = number.toString()
}
R.id.button_reset -> {
// 按下 Reset 時,直接將數值設為零。
val number = 0
textViewCounter.text = number.toString()
}
}
}
}
}
參考資料:Android Developers-處理輸入事件
TextView.text 的型別是 CharSequence,不能直接執行整數加法。因此計數器需要完成以下轉換:
val data = textViewCounter.text.toString()
var number = data.toInt()
number++
textViewCounter.text = number.toString()
轉換流程為:
TextView.text
→ CharSequence
→ String
→ Int
→ 執行數學運算
→ String
→ 顯示回 TextView
如果畫面中的文字不一定是有效數字,可以改用 toIntOrNull(),避免格式錯誤造成 App 閃退:
val number = textViewCounter.text
.toString()
.toIntOrNull()
?: 0
當轉換失敗時,toIntOrNull() 會回傳 null,而 Elvis 運算子 ?: 會讓程式改用預設值 0。
參考資料:Kotlin-字串轉數字
在 Kotlin 中,巢狀類別預設無法直接存取外部類別的成員。
如果把 MyButton 寫成:
class MyButton : View.OnClickListener
它便不能直接使用 MainActivity 裡的 textViewCounter。
加上 inner 後:
inner class MyButton : View.OnClickListener
MyButton 物件會持有外部 MainActivity 的參考,因此可以操作 Activity 的畫面元件。
參考資料:Kotlin-Nested and inner classes
三個按鈕使用相同監聽器,所以必須知道實際被點擊的是哪一個 View。
onClick() 參數中的 view 就是被點擊的元件,因此可以讀取:
view?.id
再交給 when 判斷:
when (view?.id) {
R.id.button_plus -> {
// 執行增加
}
R.id.button_minus -> {
// 執行減少
}
R.id.button_reset -> {
// 執行歸零
}
}
這種寫法適合多個按鈕行為相似,或需要統一管理事件的情況。
參考資料:Kotlin-when 條件運算式
第三個範例使用一個 ImageView 顯示圖片,並提供三個按鈕。按下按鈕後,除了切換圖片,也會修改圖片說明文字。
圖片應先放進:
app/src/main/res/drawable/
例如:
drawable/img1.jpg
drawable/img2.jpg
drawable/img3.jpg
圖片資源放入 drawable 後,Android 會在 R.drawable 中產生對應的資源 ID。
參考資料:Android Developers-Drawable 資源
package com.example.widget_3
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
// 顯示目前圖片說明文字。
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge。
enableEdgeToEdge()
// 載入 activity_main.xml。
setContentView(R.layout.activity_main)
// 避免畫面被狀態列或導覽列遮住。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 取得負責顯示圖片的 ImageView。
val imageView =
findViewById<ImageView>(R.id.imageView)
// 取得三個切換圖片的按鈕。
val button1 =
findViewById<Button>(R.id.button)
val button2 =
findViewById<Button>(R.id.button2)
val button3 =
findViewById<Button>(R.id.button3)
// 取得顯示圖片說明的 TextView。
textView =
findViewById(R.id.textView)
// 按下第一個按鈕,顯示 img1 並修改說明文字。
button1.setOnClickListener {
imageView.setImageResource(
R.drawable.img1
)
textView.text = "This is a flower 1"
}
// 按下第二個按鈕,顯示 img2 並修改說明文字。
button2.setOnClickListener {
imageView.setImageResource(
R.drawable.img2
)
textView.text = "This is a flower 2"
}
// 按下第三個按鈕,顯示 img3 並修改說明文字。
button3.setOnClickListener {
imageView.setImageResource(
R.drawable.img3
)
textView.text = "This is a flower 3"
}
}
}
參考資料:Android Developers-ImageView
ImageView.setImageResource() 可以把指定的 Drawable 資源設定成 ImageView 的顯示內容。
語法如下:
imageView.setImageResource(R.drawable.img1)
其中:
imageView:準備更新的圖片元件。setImageResource():設定圖片資源的方法。R.drawable.img1:放在 drawable 目錄中的圖片資源 ID。如果要清除目前圖片,可以傳入 0:
imageView.setImageResource(0)
setImageResource() 適合切換 App 內建的小型圖片;若要讀取網路圖片或大型照片,通常會使用 Coil、Glide 等圖片載入函式庫,避免在主執行緒進行過多圖片解碼工作。
參考資料:Android Developers-ImageView.setImageResource
第四個範例把一般 Button 改為 ImageButton。畫面中共有六個圖片按鈕與三個 TextView,每兩個圖片按鈕控制一個文字區域。
例如:
| ImageButton | 修改的 TextView | 顯示內容 |
|---|---|---|
imageButton1 |
tv1 |
this is flower 1 |
imageButton2 |
tv2 |
this is flower 2 |
imageButton3 |
tv3 |
this is flower 3 |
imageButton4 |
tv1 |
this is flower 4 |
imageButton5 |
tv2 |
this is flower 5 |
imageButton6 |
tv3 |
this is flower 6 |
這個範例同時使用 View Binding 與共用的 MyImgButton 事件監聽器。
參考資料:Android Developers-ImageButton|Android Developers-View Binding
package com.example.widget_41f
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.widget_41f.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// Activity 對應的 View Binding。
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge。
enableEdgeToEdge()
// 建立 activity_main.xml 對應的 Binding 物件。
binding = ActivityMainBinding.inflate(layoutInflater)
// 將 Binding 的根 View 設為 Activity 畫面。
setContentView(binding.root)
// 處理狀態列及導覽列範圍。
ViewCompat.setOnApplyWindowInsetsListener(
binding.main
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 設定第一個 TextView 的初始內容。
binding.tv1.text = "1234"
// 建立一個共用的圖片按鈕監聽器。
// 重複使用同一個物件,可避免建立六個不同的監聽器物件。
val imageButtonListener = MyImgButton()
// 六個 ImageButton 全部使用相同的事件監聽器。
binding.imageButton1.setOnClickListener(imageButtonListener)
binding.imageButton2.setOnClickListener(imageButtonListener)
binding.imageButton3.setOnClickListener(imageButtonListener)
binding.imageButton4.setOnClickListener(imageButtonListener)
binding.imageButton5.setOnClickListener(imageButtonListener)
binding.imageButton6.setOnClickListener(imageButtonListener)
}
/**
* 六個 ImageButton 共用的事件監聽器。
*
* 透過 inner class 存取 Activity 的 binding,
* 再使用 when 判斷是哪個圖片按鈕被點擊。
*/
inner class MyImgButton : View.OnClickListener {
override fun onClick(view: View?) {
when (view?.id) {
// 點擊第一張圖片,修改 tv1。
R.id.imageButton1 -> {
binding.tv1.text = "this is flower 1"
}
// 點擊第二張圖片,修改 tv2。
R.id.imageButton2 -> {
binding.tv2.text = "this is flower 2"
}
// 點擊第三張圖片,修改 tv3。
R.id.imageButton3 -> {
binding.tv3.text = "this is flower 3"
}
// 點擊第四張圖片,再次修改 tv1。
R.id.imageButton4 -> {
binding.tv1.text = "this is flower 4"
}
// 點擊第五張圖片,再次修改 tv2。
R.id.imageButton5 -> {
binding.tv2.text = "this is flower 5"
}
// 點擊第六張圖片,再次修改 tv3。
R.id.imageButton6 -> {
binding.tv3.text = "this is flower 6"
}
}
}
}
}
原始寫法為每個按鈕各建立一個 MyImgButton():
binding.imageButton1.setOnClickListener(MyImgButton())
binding.imageButton2.setOnClickListener(MyImgButton())
binding.imageButton3.setOnClickListener(MyImgButton())
binding.imageButton4.setOnClickListener(MyImgButton())
binding.imageButton5.setOnClickListener(MyImgButton())
binding.imageButton6.setOnClickListener(MyImgButton())
這樣可以正常執行,但會建立六個 MyImgButton 物件。
可以先建立一個物件,再讓六個按鈕共同使用:
val imageButtonListener = MyImgButton()
binding.imageButton1.setOnClickListener(imageButtonListener)
binding.imageButton2.setOnClickListener(imageButtonListener)
binding.imageButton3.setOnClickListener(imageButtonListener)
binding.imageButton4.setOnClickListener(imageButtonListener)
binding.imageButton5.setOnClickListener(imageButtonListener)
binding.imageButton6.setOnClickListener(imageButtonListener)
兩種寫法功能相同,但共用物件更能表達「這些按鈕由同一個監聽器統一管理」的設計。
參考資料:Android Developers-View.setOnClickListener
| 比較項目 | ImageView | ImageButton |
|---|---|---|
| 主要用途 | 顯示圖片 | 使用圖片作為按鈕 |
| 是否預期可以點擊 | 通常只顯示 | 預設用途就是互動 |
| 常見情境 | 商品圖、照片、封面 | 播放、刪除、上一張、下一張 |
| 圖片設定 | android:src |
android:src |
| 點擊事件 | 可以自行加入 | 通常會設定 |
| 無障礙說明 | 建議設定 | 必須特別注意 |
雖然 ImageView 也可以加入 setOnClickListener(),但如果元件的主要用途就是按鈕,使用 ImageButton 會更清楚。
圖片按鈕沒有文字標籤,因此 XML 中應加入 android:contentDescription,讓 TalkBack 等無障礙服務可以說明按鈕功能:
<ImageButton
android:id="@+id/imageButton1"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/img1"
android:contentDescription="@string/show_flower_1" />
strings.xml:
<resources>
<string name="app_name">Widget Demo</string>
<string name="show_flower_1">顯示第一張花朵圖片</string>
</resources>
參考資料:Android Developers-ImageButton|Android Developers-描述 UI 元素
| 範例 | 元件 | 取得元件方式 | 事件處理方式 |
|---|---|---|---|
| 範例一 | Button、TextView | View Binding | 每個按鈕各自使用 Lambda |
| 範例二 | Button、TextView | findViewById() |
三個按鈕共用 MyButton |
| 範例三 | Button、ImageView | findViewById() |
每個按鈕各自使用 Lambda |
| 範例四 | ImageButton、TextView | View Binding | 六個按鈕共用 MyImgButton |
如果每個按鈕的功能差異很大,而且程式碼不多,可以為每個按鈕分別撰寫 Lambda。如果有許多同類按鈕,且處理流程相似,則可以使用共用監聽器,再透過 when 判斷 View ID。
參考資料:Android Developers-回應點擊事件
今天的程式同時使用了兩種取得元件的方法。
val imageView =
findViewById<ImageView>(R.id.imageView)
優點是容易理解 XML ID 與 Kotlin 元件之間的關係,但每一個元件都需要手動取得。
binding.imageButton1.setOnClickListener {
// 處理事件
}
View Binding 會直接產生元件屬性,具有較好的型別安全與空值安全,也能減少重複程式碼。
Android 官方指出,在大多數使用情境中,View Binding 可以取代 findViewById()。對初學者而言,兩種方式都值得理解;建立新專案時,則可以優先使用 View Binding。
參考資料:Android Developers-View Binding 與 findViewById 的差異
今天透過四個實作範例,完成了 Android 常用 Widget 與點擊事件的練習:
TextView 顯示與更新文字、數字。Button 接收使用者的點擊操作。ImageView 顯示 Drawable 圖片。setImageResource() 動態切換圖片。ImageButton 製作圖片式按鈕。findViewById() 取得 XML 元件。View.OnClickListener 建立共用監聽器。when 依照 View ID 分流處理。inner class 存取 Activity 成員。這些元件看似簡單,卻是 Android App 人機互動的基礎。無論日後製作購物車數量調整、圖片瀏覽器、音樂播放器,或各種功能選單,都會使用到今天學習的事件處理觀念。
下一篇可以繼續認識 EditText、CheckBox、RadioButton、Switch 與 Toast,讓 App 不只能接收點擊,也能取得使用者輸入及顯示操作結果。
參考資料:Android Developers-使用 Views 建構 UI|Android Developers-輸入事件